﻿/*
    This software is licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php

    Copyright (C) 2010 Daniel Deptford (redmercury@gmail.com)
 
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in
        all copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
        THE SOFTWARE.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Xml;

namespace COLLADASharp.Serialization
{
    class ClassSerializer : ElementSerializerBase
    {
        private Type classType;
        private Dictionary<NodeProperty, FieldSerializerElementBase> elementSerializers = new Dictionary<NodeProperty, FieldSerializerElementBase>();
        private Dictionary<string, NodeProperty> elementProperties = new Dictionary<string, NodeProperty>();
        private List<Dictionary<string, NodeProperty>> orderedElementSerializers = new List<Dictionary<string, NodeProperty>>();
        private Dictionary<string, FieldAttributeSerializer> attributeSerializers = new Dictionary<string, FieldAttributeSerializer>();

        public ClassSerializer(Type classType)
        {
            this.classType = classType;
        }

        internal override void Initialize()
        {
            Dictionary<string, int> elementOrders = new Dictionary<string, int>();

            List<FieldInfo> allFields = new List<FieldInfo>();

            for (Type t = classType; t != null; t = t.BaseType)
            {
                allFields.AddRange(t.GetFields(BindingFlags.Static | BindingFlags.NonPublic));
            }

            foreach (var field in allFields)
            {
                DAEElementAttribute[] elementAttributes = field.GetCustomAttributes(typeof(DAEElementAttribute), true).Cast<DAEElementAttribute>().ToArray();
                if (elementAttributes != null && elementAttributes.Length != 0)
                {
                    AddElementField(field);

                    foreach (var elementAttribute in elementAttributes)
                    {
                        string elementName = elementAttribute.XmlName;
                        if (string.IsNullOrEmpty(elementName))
                        {
                            DAEClassAttribute classAttribute = elementAttribute.ElementType.GetCustomAttributes(typeof(DAEClassAttribute), false)
                                .Cast<DAEClassAttribute>()
                                .FirstOrDefault();

                            if (classAttribute == null)
                                throw new DAEException(string.Format("Field does not have an element name, and its type {0} does not have a [DAEClass] attribute", elementAttribute.ElementType));

                            elementName = classAttribute.XmlName;
                        }

                        elementOrders.Add(elementName, elementAttribute.Order);
                    }
                }

                var attributeAttributes = field.GetCustomAttributes(typeof(DAEAttributeAttribute), true);
                if (attributeAttributes != null && attributeAttributes.Length != 0)
                    AddAttributeField(field);
            }

            if (elementOrders.Count != 0)
            {
                Dictionary<int, List<string>> orderedElements = new Dictionary<int, List<string>>();

                foreach (var elementOrder in elementOrders)
                {
                    List<string> elementStrings = null;
                    if (!orderedElements.TryGetValue(elementOrder.Value, out elementStrings))
                    {
                        elementStrings = new List<string>();
                        orderedElements.Add(elementOrder.Value, elementStrings);
                    }
                    elementStrings.Add(elementOrder.Key);
                }

                var orderedElementAsArray = orderedElements.ToArray().OrderBy(v => v.Key);
                int lastOrderIdx = orderedElementAsArray.First().Key - 1;
                Dictionary<string, NodeProperty> currentDictionary = null;
                foreach (var orderedElement in orderedElementAsArray)
                {
                    if (lastOrderIdx != orderedElement.Key)
                    {
                        currentDictionary = new Dictionary<string, NodeProperty>();
                        orderedElementSerializers.Add(currentDictionary);
                    }

                    foreach (var elementName in orderedElement.Value)
                    {
                        currentDictionary[elementName] = elementProperties[elementName];
                    }
                }
            }
        }

        internal void AddElementField(FieldInfo fieldInfo)
        {
            // Retrieve the property we are serializing.
            NodeProperty property = (NodeProperty)fieldInfo.GetValue(null);

            Type fieldType = property.PropertyType;
            FieldSerializerElementBase fieldSerializer = null;

            // Get all of the elements that apply to this field.
            var elementAttributes = fieldInfo.GetCustomAttributes(typeof(DAEElementAttribute), true).Cast<DAEElementAttribute>().ToArray();
            if (IsGenericIList(fieldType, out fieldType))
            {
                fieldSerializer = new FieldSerializerElementCollection(property, elementAttributes);
            }
            else
            {
                fieldSerializer = new FieldSerializerElement(property, elementAttributes);
            }

            elementSerializers[property] = fieldSerializer;
            foreach (var elementAttribute in elementAttributes)
            {
                string elementName = elementAttribute.XmlName;
                if (string.IsNullOrEmpty(elementName))
                {
                    DAEClassAttribute classAttribute = elementAttribute.ElementType.GetCustomAttributes(typeof(DAEClassAttribute), false)
                        .Cast<DAEClassAttribute>()
                        .FirstOrDefault();

                    if (classAttribute == null)
                        throw new DAEException(string.Format("Field does not have an element name, and its type {0} does not have a [DAEClass] attribute", elementAttribute.ElementType));

                    elementName = classAttribute.XmlName;
                }

                elementProperties[elementName] = property;
            }
        }

        internal void AddAttributeField(FieldInfo fieldInfo)
        {
            NodeProperty property = (NodeProperty)fieldInfo.GetValue(null);
            DAEAttributeAttribute attributeAttribute = fieldInfo.GetCustomAttributes(typeof(DAEAttributeAttribute), true)
                .Cast<DAEAttributeAttribute>()
                .First();

            // Assuming that all attributes are type converter compliant
            AttributeSerializerBase attributeSerializer = new TypeConverterAttributeSerializer(property.PropertyType);
            FieldAttributeSerializer fieldSerializer = new FieldAttributeSerializer(property, attributeAttribute.XmlName, attributeSerializer);

            attributeSerializers[attributeAttribute.XmlName] = fieldSerializer;
        }
        
        internal static bool IsGenericIList(Type type, out Type elementType)
        {
            for (Type t = type; t != null; t = t.BaseType)
            {
                foreach (Type interfaceType in t.GetInterfaces())
                {
                    if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IList<>))
                    {
                        elementType = interfaceType.GetGenericArguments()[0];
                        return true;
                    }
                }
            }
            elementType = type;
            return false;
        }

        public override object Deserialize(XmlReader parentReader, Type parentType, DAESerializer serializer)
        {
            XmlReader reader = parentReader.ReadSubtree();
            int start_depth = reader.Depth;

            DAENode obj = (DAENode)Activator.CreateInstance(classType, true);

            reader.MoveToContent();
            for (int i = 0; i < reader.AttributeCount; i++)
            {
                reader.MoveToAttribute(i);
                FieldAttributeSerializer fieldAttributeSerializer = null;
                if(attributeSerializers.TryGetValue(reader.Name, out fieldAttributeSerializer))
                {
                    fieldAttributeSerializer.Deserialize(reader, obj, serializer);
                }
            }

            reader.MoveToElement();

            if (!reader.IsEmptyElement)
            {
                if (orderedElementSerializers.Count != 0) 
                {
                    reader.ReadStartElement();
                    int currentIdx = 0;
                    Dictionary<string, NodeProperty> currentPropertySet = orderedElementSerializers[currentIdx];

                    while (reader.NodeType != XmlNodeType.EndElement)
                    {
                        if (reader.NodeType == XmlNodeType.Element)
                        {
                            NodeProperty nodeProperty = null;
                            if (!currentPropertySet.TryGetValue(reader.Name, out nodeProperty))
                            {
                                for (int searchIdx = currentIdx + 1; searchIdx < orderedElementSerializers.Count && nodeProperty == null; searchIdx++)
                                {
                                    Dictionary<string, NodeProperty> searchPropertySet = orderedElementSerializers[searchIdx];
                                    if (searchPropertySet.TryGetValue(reader.Name, out nodeProperty))
                                    {
                                        currentPropertySet = searchPropertySet;
                                        currentIdx = searchIdx;
                                        break;
                                    }
                                }
                            }

                            if (nodeProperty != null)
                            {
                                FieldSerializerElementBase fieldSerializer = elementSerializers[nodeProperty];
                                fieldSerializer.Deserialize(reader, obj, serializer);
                            }
                            else
                            {
                                Console.WriteLine(string.Format("Skipping {0}", reader.Name));
                                reader.Skip();
                            }
                        }
                        reader.Read();
                    }
                }
                else
                {
                    reader.ReadSubtree().Close();
                }
                reader.ReadEndElement();
            }

            Debug.Assert(start_depth == reader.Depth);
            reader.Close();

            return obj;
        }

        public override void Serialize(XmlWriter writer, object obj, DAESerializer serializer)
        {
            throw new NotImplementedException();
        }

        private abstract class FieldSerializerBase
        {
            protected NodeProperty nodeProperty;

            protected FieldSerializerBase(NodeProperty nodeProperty)
            {
                this.nodeProperty = nodeProperty;
            }

            public abstract void Deserialize(XmlReader reader, DAENode node, DAESerializer serializer);
            public abstract void Serialize(XmlWriter writer, DAENode node, DAESerializer serializer);
        }

        private abstract class FieldSerializerElementBase : FieldSerializerBase
        {
            protected FieldSerializerElementBase(NodeProperty nodeProperty)
                : base(nodeProperty)
            {
            }
        }

        private class FieldSerializerElement : FieldSerializerElementBase
        {
            private Dictionary<string, ElementSerializerBase> serializersByName = new Dictionary<string, ElementSerializerBase>();
            private Dictionary<ElementSerializerBase, string> namesBySerializer = new Dictionary<ElementSerializerBase, string>();
            private Dictionary<Type, ElementSerializerBase> serializersByType = new Dictionary<Type, ElementSerializerBase>();

            public FieldSerializerElement(NodeProperty nodeProperty, DAEElementAttribute[] elementAttributes)
                : base(nodeProperty)
            {
                foreach (var elementAttribute in elementAttributes)
                {
                    ElementSerializerBase serializer = DAESerializer.GetElementSerializer(elementAttribute.ElementType);

                    if (serializer == null)
                        serializer = DAESerializer.CreateElementSerializer(elementAttribute.ElementType);

                    string elementName = elementAttribute.XmlName;
                    if (elementName == null)
                    {
                        DAEClassAttribute classAttribute = elementAttribute.ElementType.GetCustomAttributes(typeof(DAEClassAttribute), false)
                            .Cast<DAEClassAttribute>()
                            .FirstOrDefault();

                        if (classAttribute == null)
                            throw new DAEException(string.Format("Field does not have an element name, and its type {0} does not have a [DAEClass] attribute", elementAttribute.ElementType));

                        elementName = classAttribute.XmlName;
                    }

                    serializersByName.Add(elementName, serializer);
                    namesBySerializer.Add(serializer, elementName);
                    serializersByType.Add(elementAttribute.ElementType, serializer);
                }
            }

            protected object Deserialize(XmlReader reader, Type parentType, DAESerializer serializer)
            {
                ElementSerializerBase objectSerializer = serializersByName[reader.Name];
                object obj = objectSerializer.Deserialize(reader, parentType, serializer);
                return obj;
            }

            public override void Deserialize(XmlReader reader, DAENode node, DAESerializer serializer)
            {
                if (reader.IsEmptyElement)
                {
                    reader.Read();
                    return;
                }

                object obj = Deserialize(reader, node.GetType(), serializer);
                node.SetValue(nodeProperty, obj);
            }

            protected void Serialize(XmlWriter writer, object obj, DAESerializer serializer)
            {
                ElementSerializerBase objectSerializer = null;
                Type objectType = obj.GetType();
                while (objectSerializer == null && objectType != null)
                {
                    if (!serializersByType.TryGetValue(objectType, out objectSerializer))
                        objectType = objectType.BaseType;
                }

                if (objectSerializer == null)
                {
                    throw new DAEException(string.Format("Element not found for object with type {0}", obj.GetType().ToString()));
                }

                string elementName = namesBySerializer[objectSerializer];

                writer.WriteStartElement(elementName);
                objectSerializer.Serialize(writer, obj, serializer);
                writer.WriteEndElement();
            }

            public override void Serialize(XmlWriter writer, DAENode node, DAESerializer serializer)
            {
                object obj = node.GetValue(nodeProperty);
                if (obj != null)
                {
                    Serialize(writer, obj, serializer);
                }
            }
        }

        private class FieldSerializerElementCollection : FieldSerializerElement
        {
            public FieldSerializerElementCollection(NodeProperty nodeProperty, DAEElementAttribute[] elementAttributes)
                : base(nodeProperty, elementAttributes)
            {
            }

            public override void Deserialize(XmlReader reader, DAENode node, DAESerializer serializer)
            {
                IList list = node.GetValue(nodeProperty) as IList;
                if (list == null)
                {
                    list = (IList)Activator.CreateInstance(nodeProperty.PropertyType, true);
                    node.SetValue(nodeProperty, list);
                }

                object obj = Deserialize(reader, node.GetType(), serializer);
                list.Add(obj);
            }

            public override void Serialize(XmlWriter writer, DAENode node, DAESerializer serializer)
            {
                IList list = node.GetValue(nodeProperty) as IList;

                if (list != null)
                {
                    foreach (object obj in list)
                    {
                        Serialize(writer, obj, serializer);
                    }
                }
            }
        }

        private class FieldAttributeSerializer : FieldSerializerBase
        {
            private AttributeSerializerBase attributeSerializer;
            private string attributeName;

            public FieldAttributeSerializer(NodeProperty nodeProperty, string attributeName, AttributeSerializerBase attributeSerializer)
                : base(nodeProperty)
            {
                this.attributeSerializer = attributeSerializer;
                this.attributeName = attributeName;
            }

            public override void Deserialize(XmlReader reader, DAENode node, DAESerializer serializer)
            {
                object obj = attributeSerializer.Deserialize(reader, attributeName, serializer);
                node.SetValue(nodeProperty, obj);
            }

            public override void Serialize(XmlWriter writer, DAENode node, DAESerializer serializer)
            {
                object obj = node.GetValue(nodeProperty);
                attributeSerializer.Serialize(writer, attributeName, obj, serializer);
            }
        }
    }
}
